//
// Ephi - simulation of magnetic fields and particles
// Copyright (C) 2007 Indrek Mandre <indrek(at)mare.ee>
// For more information please see http://www.mare.ee/indrek/ephi/
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "ephi.hpp"

#ifdef ENABLE_PNG
#include <png.h>
#endif

#define ABSURD_Z 9e9

#define sig_width 187
#define sig_height 14
static unsigned char sig_bits[] = {
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02,
   0x02, 0x82, 0x00, 0x00, 0x08, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x22, 0x00, 0x08, 0x00, 0x04, 0x08, 0x00, 0x02, 0x02, 0x02,
   0x02, 0x82, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x21, 0x00, 0x08, 0x00, 0x04, 0x04, 0x00, 0x02, 0x02, 0x01,
   0x02, 0x82, 0x00, 0x18, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x01, 0x00, 0x08, 0x00, 0x04, 0x04, 0x00, 0x02, 0x00, 0x01,
   0x1e, 0xc7, 0x79, 0x18, 0x82, 0x88, 0xa2, 0x08, 0x58, 0x3c, 0xcd, 0x01,
   0x1c, 0x87, 0x30, 0x1a, 0x4f, 0x73, 0x24, 0xc2, 0x79, 0x1e, 0x83, 0x00,
   0x22, 0x82, 0x88, 0x00, 0x82, 0xa8, 0xaa, 0x0a, 0xa8, 0x22, 0x23, 0x02,
   0xa2, 0x88, 0x20, 0xa6, 0xc8, 0x88, 0x14, 0x22, 0x8a, 0x22, 0x82, 0x00,
   0x22, 0x82, 0x88, 0x00, 0x41, 0xa8, 0xaa, 0x0a, 0xa8, 0x22, 0xe1, 0x03,
   0xbe, 0x4f, 0x20, 0xa2, 0x48, 0xf8, 0x0c, 0xe1, 0x8b, 0x22, 0x42, 0x00,
   0x22, 0x82, 0x88, 0x18, 0x41, 0xa8, 0xaa, 0x8a, 0xa9, 0x32, 0x21, 0x60,
   0x82, 0x40, 0x20, 0xa2, 0x48, 0x08, 0x14, 0x21, 0x88, 0x22, 0x42, 0x00,
   0x22, 0x0c, 0x7b, 0x98, 0x20, 0x50, 0x14, 0x85, 0x89, 0x2c, 0xc1, 0x61,
   0x1c, 0x27, 0x70, 0x22, 0x4f, 0x70, 0xa4, 0xc0, 0x79, 0x22, 0x27, 0x00,
   0x00, 0x00, 0x08, 0x80, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x08, 0x00, 0x20, 0x00,
   0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };

Screen::Screen (size_t w, size_t h, prec_t sw, prec_t sh, const color& c) :
    width(w), height(h), space_width(sw), space_height(sh)
{
  smap = new color [w * h];
  zbuf = 0;
  dw = (prec_t)width/space_width;
  dh = (prec_t)height/space_height;
  dx = space_width/(prec_t)width;
  dy = space_height/(prec_t)height;
  is_perspective = false;
  set_axis (PLANE_XY);
  clear (c);
}

Screen::Screen (const Screen& copy) : smap(0), zbuf(0)
{
  *this = copy;
}

Screen& Screen::operator= (const Screen& copy)
{
  delete [] smap;
  delete [] zbuf;
  zbuf = 0;
  width = copy.width;
  height = copy.height;
  space_width = copy.space_width;
  space_height = copy.space_height;
  dw = copy.dw;
  dh = copy.dh;
  dx = copy.dx;
  dy = copy.dy;
  is_perspective = copy.is_perspective;
  smap = new color [width * height];
  memcpy (smap, copy.smap, width * height * sizeof (color));
  if ( copy.zbuf )
    {
      zbuf = new prec_t [width * height];
      memcpy (zbuf, copy.zbuf, width * height * sizeof (prec_t));
    }
  return *this;
}

void Screen::clear (const color& c)
{
  for ( size_t i = 0; i < width * height; i++ )
    {
      smap[i] = c;
      if ( zbuf )
          zbuf[i] = ABSURD_Z;
    }
}

Screen::~Screen ()
{
  delete [] smap;
  delete [] zbuf;
}

void Screen::set_pixel (size_t x, size_t y, const color& c)
{
  if ( x >= width || y >= height )
      return;
  smap[y * width + x] = c;
}

void Screen::set_pixel_merge (size_t x, size_t y, prec_t f, const color& c)
{
  if ( x >= width || y >= height )
      return;
  color& res = smap[y * width + x];
  vect3d pcv(res.r, res.g, res.b);
  vect3d ncv(c.r, c.g, c.b);
  vect3d cc = pcv * (1 - f) + ncv * f;
  res.r = (unsigned char)prec_t_round(cc.x - 0.00001);
  res.g = (unsigned char)prec_t_round(cc.y - 0.00001);
  res.b = (unsigned char)prec_t_round(cc.z - 0.00001);
}

void Screen::set_pixel (const vect3d& p, const color& c)
{
  prec_t z = (p * normal + plane_d);
  vect3d pos = p + normal * z - center;
  prec_t sx = xdir * pos;
  prec_t sy = ydir * pos;

  if ( is_perspective )
    {
      if ( z < 0.0 )
          return;
      prec_t zf = 1.0 / z;
      sx *= zf;
      sy *= zf;
    }

  size_t x, y;
  x = (size_t)prec2double((space_width/2 + sx) * dw);
  y = (size_t)prec2double((space_height/2 + sy) * dh);

  if ( x >= width || y >= height || zbuf && zbuf[y * width + x] <= z )
      return;

  if ( zbuf )
      zbuf[y * width + x] = z;
  smap[y * width + x] = c;
}

void Screen::set_pixel_prec (prec_t px, prec_t py, const color& c)
{
  size_t x, y;
  x = (size_t)prec2double((space_width/2 + px) * dw);
  y = (size_t)prec2double((space_height/2 + py) * dh);
  set_pixel (x, y, c);
}

void Screen::translate (const vect3d& p, size_t& x, size_t& y)
{
  // project to plane and get vector from center
  prec_t dist = p * normal + plane_d;
  vect3d pos = p - normal * dist - center;
  prec_t sx = xdir * pos;
  prec_t sy = ydir * pos;
  if ( is_perspective )
    {
      sx /= dist;
      sy /= dist;
    }
  x = (size_t)prec2double((space_width/2 + sx) * dw);
  y = (size_t)prec2double((space_height/2 + sy) * dh);
}

void Screen::translate (const vect3d& p, prec_t& x, prec_t& y, prec_t& z)
{
  prec_t dist = p * normal + plane_d;
  vect3d pos = p - normal * dist - center;
  x = xdir * pos;
  y = ydir * pos;
  z = dist;
}

void Screen::reverse (size_t x, size_t y, vect3d& p)
{
  prec_t xout = -space_width/2 + x * dx;
  prec_t yout = -space_height/2 + y * dy;
  p = center + xout * xdir + yout * ydir;
}

void Screen::draw_axes (prec_t interval, const color& c)
{
  if ( interval == 0.0 )
    {
      interval = 0.1;
      while ( 4 * interval > space_width )
          interval *= 0.5;
    }
  size_t n = 0;
  set_pixel (vect3d(0.0, 0.0, 0.0), c);
  size_t x = (size_t)prec2double((space_width/2 + 0.0) * ((prec_t)width/space_width));
  size_t y = (size_t)prec2double((space_height/2 + 0.0) * ((prec_t)height/space_height));
  set_pixel (x, y, c);
  set_pixel (x + 1, y, c);
  set_pixel (x - 1, y, c);
  set_pixel (x, y + 1, c);
  set_pixel (x, y - 1, c);
  while (true)
    {
      prec_t dist = (prec_t)n * interval;
      if ( dist > space_width / 2 && dist > space_height / 2 )
          break;
      x = (size_t)prec2double((space_width/2 + dist) * ((prec_t)width/space_width));
      y = (size_t)prec2double((space_height/2 + 0) * ((prec_t)height/space_height));
      set_pixel (x, y, c);
      set_pixel (x, y - 1, c);
      set_pixel (x, y + 1, c);
      x = (size_t)prec2double((space_width/2 - dist) * ((prec_t)width/space_width));
      y = (size_t)prec2double((space_height/2 + 0) * ((prec_t)height/space_height));
      set_pixel (x, y, c);
      set_pixel (x, y - 1, c);
      set_pixel (x, y + 1, c);
      x = (size_t)prec2double((space_width/2 + 0.0) * ((prec_t)width/space_width));
      y = (size_t)prec2double((space_height/2 + dist) * ((prec_t)height/space_height));
      set_pixel (x, y, c);
      set_pixel (x - 1, y, c);
      set_pixel (x + 1, y, c);
      x = (size_t)prec2double((space_width/2 + 0.0) * ((prec_t)width/space_width));
      y = (size_t)prec2double((space_height/2 - dist) * ((prec_t)height/space_height));
      set_pixel (x, y, c);
      set_pixel (x - 1, y, c);
      set_pixel (x + 1, y, c);
      n++;
    }
}

// little endian 16bit
static void write_short(FILE *fp, int v)
{
  unsigned char out[2];
  out[0] = (v&0x000000ff) >> 0;
  out[1] = (v&0x0000ff00) >> 8;
  fwrite (out, 2, 1, fp);
}

// little endian 32bit
static void write_int(FILE *fp, int v)
{
  unsigned char out[4];
  out[0] = (v&0x000000ff) >> 0;
  out[1] = (v&0x0000ff00) >> 8;
  out[2] = (v&0x00ff0000) >> 16;
  out[3] = (v&0xff000000) >> 24;
  fwrite (out, 4, 1, fp);
}

void Screen::write (const char *path)
{
  // first insert signature
  if ( sig_width <= width )
    {
      size_t rw = sig_width;
      if ( sig_width % 8)
          rw += 8 - sig_width % 8;
      for ( size_t w = 0; w < sig_width; w++ )
        {
          for ( size_t h = 0; h < sig_height; h++ )
            {
              size_t index = h * rw + w;
              size_t bit = 1 & ((sig_bits[index/8])>>(index%8));
              set_pixel (w, sig_height - h - 1, bit ? WHITE : BLACK);
            }
        }
    }

  FILE *fp = fopen (path, "wb");
  if ( !fp )
      return;

  if ( strlen(path) > 4 && (strcmp (path+strlen(path)-4, ".png") == 0 ||
      strcmp (path+strlen(path)-4, ".PNG") == 0) )
    {
#ifdef ENABLE_PNG
      png_structp png_ptr;
      png_infop info_ptr;
      png_text text;
      png_bytep *row_pointers;

      png_ptr = png_create_write_struct (PNG_LIBPNG_VER_STRING, 0, 0, 0);
      info_ptr = png_create_info_struct (png_ptr);

      if (setjmp(png_ptr->jmpbuf))
        {
          png_destroy_write_struct (&png_ptr, &info_ptr);
          fclose(fp);
          Ephi::debug (Ephi::DEBUG_ERROR, "Screen::write: png write failed\n");
          return;
        }

      png_init_io (png_ptr, fp);

      png_set_IHDR (png_ptr, info_ptr, width, height, 8,
          PNG_COLOR_TYPE_RGB, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

      memset (&text, 0, sizeof (png_text));
      text.key = "Generator";
      text.text = "Ephi (http://www.mare.ee/indrek/ephi/)";
      text.compression = PNG_TEXT_COMPRESSION_NONE;
      png_set_text (png_ptr, info_ptr, &text, 1);

      png_write_info (png_ptr, info_ptr);

      row_pointers = (png_bytep*)malloc (sizeof(png_bytep)*height);

      for ( size_t i = 0; i < height; i++ )
          row_pointers[height - i - 1] = (png_bytep)(smap + i * width);

      png_set_rows (png_ptr, info_ptr, row_pointers);

      png_write_png (png_ptr, info_ptr, PNG_TRANSFORM_BGR, 0);

      png_destroy_write_struct (&png_ptr, &info_ptr);
      free (row_pointers);
#else
      Ephi::debug (Ephi::DEBUG_ERROR, "Screen::write: png support not enabled\n");
      return;
#endif
    }
  else
    {
      fwrite ("BM", 2, 1, fp);
      write_int (fp, width * height * 3 + 54);
      write_int (fp, 0);
      write_int (fp, 54);
      write_int (fp, 40);
      write_int (fp, width);
      write_int (fp, height);
      write_short (fp, 1);
      write_short (fp, 3 * 8);
      write_int (fp, 0);
      write_int (fp, width * height * 3);
      write_int (fp, 0);
      write_int (fp, 0);
      write_int (fp, 0);
      write_int (fp, 0);
      size_t padding = 0;
      if ( (width * 3) % 4 )
          padding = 4 - ((width * 3) % 4);
      const char pb[] = {0, 0, 0, 0};
      for ( size_t i = 0; i < height; i++ )
        {
          fwrite (smap + i * width, width * 3, 1, fp);
          fwrite (pb, padding, 1, fp);
        }
    }
  fclose (fp);

  Ephi::debug (Ephi::DEBUG_INFO, "Written: %s\n", path);
}

void Screen::set_axis (eplane plane, prec_t distance)
{
  switch (plane)
    {
      case PLANE_XY:
        set_axis (vect3d(0, 0, distance), vect3d(1, 0, 0), vect3d(0, 1, 0));
        break;
      case PLANE_YZ:
        set_axis (vect3d(distance, 0, 0), vect3d(0, 1, 0), vect3d(0, 0, 1));
        break;
      case PLANE_ZX:
        set_axis (vect3d(0, distance, 0), vect3d(0, 0, 1), vect3d(1, 0, 0));
        break;
      case UNDEFINED:
        abort();
        break;
    }
  plane_type = plane;
}

void Screen::set_axis (const vect3d& pos0, const vect3d& xaxis, const vect3d& yaxis)
{
  char b1[128], b2[128], b3[128];
  Ephi::debug (Ephi::DEBUG_INFO, "Screen::set_axis: p0=%s, x=%s, y=%s\n",
      pos0.sprint(b1), xaxis.sprint(b2), yaxis.sprint(b3));
  this->center = pos0;
  this->xdir = xaxis;
  this->ydir = yaxis;
  normal = xdir % ydir;
  plane_d = -(normal * pos0);
  plane_type = UNDEFINED;
}

void Screen::set_zbuffer (bool enable)
{
  if ( !enable )
    {
      delete [] zbuf;
      zbuf = 0;
      return;
    }

  zbuf = new prec_t [width * height];
  for ( size_t i = 0; i < width * height; i++ )
      zbuf[i] = ABSURD_Z;
}

void Screen::draw_line (const vect3d& p0, const vect3d& p1, const color& c)
{
  vect3d v = p1 - p0;
  prec_t t = 0;
  prec_t tinc = 0.1;
  size_t x0, y0;
  translate (p0, x0, y0);
  while (true)
    {
      size_t x, y;
      vect3d p0s1 = p0 + v * tinc;
      translate (p0 + v * tinc, x, y);
      if ( x != x0 || y != y0 )
        {
          tinc = tinc * 0.5;
          continue;
        }
      break;
    }
  tinc = tinc * 0.5;

  while ( t <= 1.0 )
    {
      set_pixel (p0 + v * t, c);
      t = t + tinc;
    }
}

void Screen::draw_cross (const vect3d& p0, const color& c, prec_t len)
{
  draw_line (p0 - 0.5 * len * vect3d(1, 0, 0), p0 + 0.5 * len * vect3d(1, 0, 0), c);
  draw_line (p0 - 0.5 * len * vect3d(0, 1, 0), p0 + 0.5 * len * vect3d(0, 1, 0), c);
  draw_line (p0 - 0.5 * len * vect3d(0, 0, 1), p0 + 0.5 * len * vect3d(0, 0, 1), c);
}

const Screen::color Screen::RED = color(255, 0 ,0);
const Screen::color Screen::GREEN = color(0, 255, 0);
const Screen::color Screen::BLUE = color(0, 0, 255);
const Screen::color Screen::WHITE = color(255, 255, 255);
const Screen::color Screen::BLACK = color(0, 0, 0);

